/*
 * Copyright 2002-2005 the original author or authors.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package net.bingosoft.common.json;

import java.beans.BeanInfo;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import net.bingosoft.common.BeanHelper;
import net.bingosoft.common.config.Config;
import net.bingosoft.common.util.ClassUtil;

import org.apache.commons.lang.StringUtils;
import org.apache.struts.action.ActionForm;


/**
 * 2007-11-30 下午12:45:09
 * Author:yangdongyu(yangdy@bingosoft.net)
 * ReadMe:
 * ChangeLog:
 */
@SuppressWarnings("unchecked")
public class JSONConverter {
	private static JSONConverter coverter =  new JSONConverter();
	private static String exculdeProperties = Config.getString("JSONCONVERTER_EXCULDEPROPERTIES", ",class,multipartRequestHandler,resultValueMap,servlet,servletWrapper,validatorResults,");

	/**
	 * 将一个Java对象转换成JSON对象,支持数组、集合和Map
	 * @param objectSource
	 * @param excludeProperties
	 * @return
	 * @throws Exception
	 */
	public static Object toJSON(Object objectSource,PropertyProcessor processor,String ... excludeProperties) throws Exception {
		if(objectSource instanceof JSONObject || objectSource instanceof JSONArray || objectSource == null) return objectSource;

		Object jsonResult = new Object();
		Class clazz = objectSource.getClass();
		if (objectSource instanceof Number || objectSource instanceof Boolean || objectSource instanceof String || objectSource instanceof Character) {
			jsonResult = objectSource;
		} else if (clazz.isArray()) {
			JSONArray jsonArray = new JSONArray();
			int length = Array.getLength(objectSource);
			for (int i = 0; i < length; i++) {
				jsonArray.put(toJSON(Array.get(objectSource, i),processor));
			}
			jsonResult = jsonArray;
		} else if (objectSource instanceof Collection) {
			Collection theCollection = (Collection) objectSource;
			Iterator iter = theCollection.iterator();
			List tempList = new ArrayList();
			while (iter.hasNext()) {
				Object element = (Object) iter.next();
				tempList.add(toJSON(element,processor));
			}
			jsonResult = new JSONArray(tempList);
		} else if (objectSource instanceof Map) {
			Map map = (Map) objectSource;
			JSONObject mapJSONObject = new JSONObject();
			Iterator iter = map.keySet().iterator();
			while (iter.hasNext()) {
				String key = (String) iter.next();
				mapJSONObject.put(key, toJSON(map.get(key),processor));
			}
			jsonResult = mapJSONObject;
		} else if(objectSource instanceof Date){
			jsonResult = BeanHelper.formatDate((Date)objectSource);
		}else{
			// for normal beans
			BeanInfo beanInfo = Introspector.getBeanInfo(coverter.getOriginalClass(objectSource.getClass()));
			JSONObject beanJSONObject = new JSONObject();
			PropertyDescriptor[] properties = beanInfo.getPropertyDescriptors();
			String excludeProps = "," + StringUtils.join(excludeProperties,",") + ",";
			for (int i = 0; i < properties.length; i++) {
				String propertyName = properties[i].getName();
				if (propertyName.equals("class") || excludeProps.contains("," +propertyName + ",")) {
					continue;
				} else {
					Method readMethod = properties[i].getReadMethod();
					if (readMethod != null) {
						Object value = readMethod.invoke(objectSource, new Object[] {});
						if (value != null) {
							beanJSONObject.put(propertyName, (processor != null) ? processor.process(objectSource,propertyName,value) : value);
						}
					}
				}
			}
			jsonResult = beanJSONObject;
		}
		return jsonResult;
	}

	/**
	 * 讲一个json对象,转换成java对象
	 * @param jsonSource json对象
	 * @param target java对象
	 * @return
	 * @throws Exception
	 */
	public static Object toObject(Object jsonSource, Object target,PropertyProcessor processor) throws Exception {
		return toObject(jsonSource, target, null,processor);
	}

	public static Object toObject(Object jsonSource, Object targetJavaBean, Class targetType,PropertyProcessor processor) throws Exception {
		if (jsonSource != null && (targetJavaBean != null ||targetType != null )) {
			if(targetType == null) {
				targetType = targetJavaBean.getClass();
			} 
			if (targetJavaBean instanceof String) {
				targetJavaBean = jsonSource;
			}else if(ClassUtil.isPrimitiveOrWrapper(targetType) && (jsonSource instanceof String || ClassUtil.isPrimitiveOrWrapper(jsonSource.getClass()))) {//处理基本数据类型
				targetJavaBean = (targetType.isInstance(jsonSource)) ? jsonSource :BeanHelper.toTargetType(String.valueOf(jsonSource), targetType);
			} else if (targetJavaBean != null && targetJavaBean.getClass().isArray() && jsonSource instanceof JSONArray) {
				JSONArray jsonArray = (JSONArray) jsonSource;
				for (int i = 0; i < jsonArray.length(); i++) {
					Array.set(targetJavaBean, i, getTypedValue(targetJavaBean.getClass().getComponentType(), jsonArray, i));
				}
			} else if (Collection.class.isAssignableFrom(targetType) && jsonSource instanceof JSONArray) {
				JSONArray jsonArray = (JSONArray) jsonSource;
				for (int i = 0; i < jsonArray.length(); i++) {
					//TODO fix me
					((Collection) targetJavaBean).add(jsonArray.get(i));
				}
			} else if (Map.class.isAssignableFrom(targetType) && jsonSource instanceof JSONObject) {
				JSONObject mapJSONObject = (JSONObject) jsonSource;
				Iterator keys = mapJSONObject.keys();
				while (keys.hasNext()) {
					String key = (String) keys.next();
					//TODO fix me
					((Map) targetJavaBean).put(key, mapJSONObject.get(key));
				}
			} else if (jsonSource instanceof JSONObject) {
				// for normal beans
				JSONObject beanJSONObject = (JSONObject) jsonSource;
				BeanInfo beanInfo = (targetJavaBean instanceof ActionForm) ?Introspector.getBeanInfo(targetJavaBean.getClass(),ActionForm.class) :Introspector.getBeanInfo(targetJavaBean.getClass()) ;
				PropertyDescriptor[] properties = beanInfo.getPropertyDescriptors();
				for (int i = 0; i < properties.length; i++) {
					String propertyName = properties[i].getName();
					if (exculdeProperties.contains(","+propertyName+",")||!beanJSONObject.has(propertyName)){
						continue;
					}
					Object jsonValue = beanJSONObject.get(propertyName);
					Object targetPropertyValue = properties[i].getReadMethod().invoke(targetJavaBean, new Object[] {});
					Class type = properties[i].getPropertyType();
					if(List.class.isAssignableFrom(type)){
						targetPropertyValue = new ArrayList();
					}else if(Map.class.isAssignableFrom(type)){
						targetPropertyValue = new HashMap();
					}else if(type.isArray()){
						if(jsonValue instanceof JSONArray){
							Class componentType = type.getComponentType();
							targetPropertyValue = ClassUtil.createArrayInstance(componentType, ((JSONArray)jsonValue).length());
						}else{
							continue;
						}
					}else if(targetPropertyValue == null && !ClassUtil.isPrimitiveOrWrapper(type) && !type.isInterface() && type != String.class) {
						Constructor constructor =  type.getConstructor();
						if(constructor!=null){
							targetPropertyValue = constructor.newInstance();
						}else{
							throw new RuntimeException("No default Constructor defined for class " + type.getName());
						}
					}
					Object unmarshalledProp = null;
					if(type == String.class){
						unmarshalledProp = jsonValue;
					}else{
						unmarshalledProp = (processor != null ) ? processor.process(targetJavaBean,propertyName, jsonValue): toObject(jsonValue, targetPropertyValue, type,processor);
					}
					if (properties[i].getWriteMethod() != null) {
						properties[i].getWriteMethod().invoke(targetJavaBean, new Object[] { unmarshalledProp });
					}
				}				
			}
		}
		return targetJavaBean;
	}

	/**
	 * @param type
	 * @param jsonArray
	 * @param index
	 * @return
	 */
	public static Object getTypedValue(Class type, JSONArray jsonArray, int index) throws Exception {
		if (jsonArray.isNull(index)) {
			return null;
		}
		if(type == JSONObject.class || type == JSONArray.class){
			return jsonArray.opt(index);
		}else if (type == int.class || type == Integer.class) {
			return new Integer(jsonArray.getInt(index));
		} else if (type == long.class || type == Long.class) {
			return new Long((long) jsonArray.getDouble(index));
		} else if (type == short.class || type == Short.class) {
			return new Short((short) jsonArray.getInt(index));
		} else if (type == byte.class || type == Byte.class) {
			return new Byte((byte) jsonArray.getInt(index));
		} else if (type == float.class || type == Float.class) {
			return new Float((float) jsonArray.getDouble(index));
		} else if (type == double.class || type == Double.class) {
			return new Double(jsonArray.getDouble(index));
		} else if (type == boolean.class || type == Boolean.class) {
			return new Boolean(jsonArray.getBoolean(index));
		} else if (type == char.class || type == Character.class) {
			return new Character(jsonArray.getString(index).charAt(0));
		} else if (type == String.class) {
			return jsonArray.getString(index);
		} else if (type == BigDecimal.class) {
			return new BigDecimal(jsonArray.getString(index).trim());
		} else if (type == BigInteger.class) {
			return new BigDecimal(jsonArray.getString(index).trim()).toBigInteger();
		} else if (type == boolean[].class || type == Boolean[].class) {
			return toObject(jsonArray.get(index), new boolean[((JSONArray) jsonArray.get(index)).length()],null);
		} else if (type == char[].class || type == Character[].class) {
			return toObject(jsonArray.get(index), new char[((JSONArray) jsonArray.get(index)).length()],null);
		} else if (type == byte[].class ||type == Byte[].class ) {
			return toObject(jsonArray.get(index), new byte[((JSONArray) jsonArray.get(index)).length()],null);
		} else if (type == short[].class || type == Short[].class ) {
			return toObject(jsonArray.get(index), new short[((JSONArray) jsonArray.get(index)).length()],null);
		} else if (type == int[].class ||type == Integer[].class) {
			return toObject(jsonArray.get(index), new int[((JSONArray) jsonArray.get(index)).length()],null);
		} else if (type == long[].class || type == Long[].class) {
			return toObject(jsonArray.get(index), new long[((JSONArray) jsonArray.get(index)).length()],null);
		} else if (type == float[].class || type == Float[].class) {
			return toObject(jsonArray.get(index), new float[((JSONArray) jsonArray.get(index)).length()],null);
		} else if (type == double[].class || type == Double[].class) {
			return toObject(jsonArray.get(index), new double[((JSONArray) jsonArray.get(index)).length()],null);
		} else {
			// return null;
			return toObject(jsonArray.get(index), createInstance(type, jsonArray.get(index)),null);
		}
	}

	private static Object createInstance(Class clazz, Object value) throws Exception {
		int length = 0;
		if (value instanceof JSONArray) {
			length = ((JSONArray) value).length();
		}
		if (clazz == Map.class) {
			return new HashMap();
		} else if (clazz == Set.class) {
			return new HashSet();
		} else if (clazz == List.class) {
			return new ArrayList();
		} else if (clazz == Collection.class) {
			return new ArrayList();
		} else if (clazz == String[].class) {
			return new String[length];
		} else if (clazz == String[][].class) {
			return new String[length][length];
		}
		return clazz.newInstance();
	}

	/**
	 * In Spring AOP, a bean post-processor may need to know that a bean is
	 * being proxied by CGLIB and if so, get to the base class that CGLIB is
	 * proxying. The reason has to do with annotations. Apparently, when CGLIB
	 * proxies a class, clazz.isAnnotationPresent() (in Java 5) no longer
	 * returns true for annotations that *are* present on the base class.
	 * Probably a limitation with annotations & proxying.
	 * 
	 * @throws ClassNotFoundException
	 */
	private Class getOriginalClass(Class clazz) throws ClassNotFoundException {
		String className = clazz.getName();
		if (className.indexOf("$$") == -1) {
			return clazz;
		} else {
			Class originalClass = Class.forName(className.substring(0, className.indexOf("$$")));
			return originalClass;
		}
	}
	/**
	 * 
	 * 2007-11-28 下午04:19:00
	 * Author:yangdongyu(yangdy@bingosoft.net)
	 * ReadMe:bean 属性的处理类
	 * ChangeLog:
	 */
	public static class PropertyProcessor{
		/**
		 * 处理bean的属性,在java bean 转换成json对象的时候,可能需要对(bean)的属性(propName) 的值(value)进行转换,如日期的转换
		 * 在json转换成javabean 时,可能对json对象的值(value)进行转换
		 * @param bean
		 * @param propName
		 * @param value
		 * @return
		 */
		public Object process(Object bean,String propName,Object value){
			return value;
		}
	}
}
